/* vim: set ts=2 et sw=2 cindent fo=qroca: */

package com.globant.google.mendoza;

import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.globant.google.mendoza.malbec.schema._2.MerchantCalculationCallback;
import com.globant.google.mendoza.malbec.schema._2.MerchantCalculationResults;
import com.globant.google.mendoza.malbec.schema._2.AnonymousAddress;
import com.globant.google.mendoza.malbec.schema._2.Calculate;
import com.globant.google.mendoza.malbec.schema._2.Method;
import com.globant.google.mendoza.malbec.schema._2.Result;

/** Validates the Merchant calculation.
 */
public final class MendozaMerchantCalculationValidator {

  /** The class logger.
   */
  private static Log log =
      LogFactory.getLog(MendozaMerchantCalculationValidator.class);

  /** The expected merchant calculation response time.
   */
  private long calculationTimeout = -1;

  /** Constructor. Creates an instance of
   * MendozaMerchantCalculationValidator.
   *
   * @param maxCalculationTime The max merchant calculation response time.
   */
  public MendozaMerchantCalculationValidator(
          final long maxCalculationTime) {
    calculationTimeout = maxCalculationTime;
  }

  /** Validates the results of the merchant calculation.
   *
   * @param merchantCalculation The merchant calculation to validate. Cannot be
   * null.
   *
   * @return Returns the calculation validation.
   */
  public MendozaMerchantCalculationValidation validate(
      final MendozaMerchantCalculation merchantCalculation) {
    if (merchantCalculation == null) {
      throw new IllegalArgumentException(
          "The merchant calculation to validate cannot be null");
    }
    log.trace("Entering validate");

    List<String> requestedAddressIdList =
      getRequestedAddressIdList(merchantCalculation);
    List<String> requestedMethodNameList =
      getRequestedMethodNameList(merchantCalculation);
    List<Result> resultList = getResultList(merchantCalculation);

    MendozaMerchantCalculationValidation calculationValidation =
      new MendozaMerchantCalculationValidation(calculationTimeout);
    validateResultSize(resultList, requestedAddressIdList,
        requestedMethodNameList, calculationValidation);
    validateResponseTime(merchantCalculation, calculationValidation);
    validateAddressResponse(requestedAddressIdList, resultList,
        calculationValidation);
    validateMethodResponse(requestedMethodNameList, resultList,
        calculationValidation);
    log.trace("Leaving validate");
    return calculationValidation;
  }

  /** Validates that the calculation result contains all
   *  the requested method names.
   *
   * @param requestedMethodNameList The requested method names.
   *
   * @param resultList The calculation result list.
   *
   * @param calculationValidation The validation result.
   */
  private void validateMethodResponse(
      final List<String> requestedMethodNameList,
      final List<Result> resultList,
      final MendozaMerchantCalculationValidation calculationValidation) {
    log.trace("Entering validateMethodResponse");
    Set<String> responseMethodNameSet =
      getResponseMethodNameSet(resultList);
    boolean missingMethodResponses =
      !responseMethodNameSet.containsAll(requestedMethodNameList);
    if (missingMethodResponses) {
      Set<String> missingMethods =
        getMissingResponse(requestedMethodNameList, responseMethodNameSet);
      calculationValidation.addMissingRequestedShipping(missingMethods);
    }
    responseMethodNameSet.removeAll(requestedMethodNameList);
    if (responseMethodNameSet.size() > 0) {
      calculationValidation.addNotRequestedShipping(responseMethodNameSet);
    }
    log.trace("Leaving validateMethodResponse");
  }

  /** Validates that the calculation result contains all
   *  the requested address ids.
   *
   * @param requestedAddressIdList The requested address ids.
   *
   * @param resultList The calculation result list.
   *
   * @param calculationValidation The validation result.
   */
  private void validateAddressResponse(
      final List<String> requestedAddressIdList,
      final List<Result> resultList,
      final MendozaMerchantCalculationValidation calculationValidation) {
    log.trace("Entering validateAddressResponse");
    // Uses Set to avoid duplicates.
    Set<String> responseAddressIdSet =
      getResponseAddressIdSet(resultList);
    boolean missingAddressResponses =
      !responseAddressIdSet.containsAll(requestedAddressIdList);
    if (missingAddressResponses) {
      Set<String> missingIds =
        getMissingResponse(requestedAddressIdList, responseAddressIdSet);
      calculationValidation.addMissingRequestedAddress(missingIds);
    }
    responseAddressIdSet.removeAll(requestedAddressIdList);
    if (responseAddressIdSet.size() > 0) {
      calculationValidation.addNotRequestedAddress(responseAddressIdSet);
    }
    log.trace("Leaving validateAddressResponse");
  }

  /** Gets the elements that not exists in the result but
   * were requested in the callback.
   *
   * @param requestedElements The requested elements.
   *
   * @param responseElements The requested elements.
   *
   * @return missing elements in the calculation response.
   */
  private Set<String> getMissingResponse(
      final List<String> requestedElements,
      final Set<String> responseElements) {
    log.trace("Entering validateAddressResponse");
    Set<String> responseAux = new HashSet<String>();
    responseAux.addAll(responseElements);
    List<String> requestAux = new ArrayList<String>();
    requestAux.addAll(requestedElements);
    responseAux.retainAll(requestAux);
    requestAux.removeAll(responseAux);
    log.trace("Leaving getMissingResponse");
    return responseAux;
  }

  /** Gets the method names of the calculation response.
   *
   * @param resultList The calculation results.
   *
   * @return Returns the method names of the calculation response.
   */
  private Set<String> getResponseMethodNameSet(
      final List<Result> resultList) {
    log.trace("Entering getResponseMethodNameSet");
    Set<String> result = new HashSet<String>();
    for (Result calculationResult : resultList) {
      String resultShippingName = calculationResult.getShippingName();
      result.add(resultShippingName);
    }
    log.trace("Leaving getResponseMethodNameSet");
    return result;
  }

  /** Gets the address ids of the calculation response.
   *
   * @param resultList The calculation results.
   *
   * @return Returns the address ids of the calculation response.
   */
  private Set<String> getResponseAddressIdSet(
      final List<Result> resultList) {
    log.trace("Entering getResponseAddressIdSet");
    Set<String> result = new HashSet<String>();
    for (Result calculationResult : resultList) {
      String resultAddressId = calculationResult.getAddressId();
      result.add(resultAddressId);
    }
    log.trace("Leaving getResponseAddressIdSet");
    return result;
  }

  /** Validates the size of the calculation result.
   *
   * @param resultList The calculation results.
   *
   * @param requestedAddressIdList The requested address ids.
   *
   * @param requestedMethodNameList The requested method names.
   *
   * @param calculationValidation The validation result.
   */
  private void validateResultSize(final List<Result> resultList,
      final List<String> requestedAddressIdList,
      final List<String> requestedMethodNameList,
      final MendozaMerchantCalculationValidation calculationValidation) {
    log.trace("Entering validateResultSize");
    int resultSize = resultList.size();
    int expectedResultSize =
      (requestedAddressIdList.size() * requestedMethodNameList.size());
    boolean correctSize = true;
    if (resultSize < expectedResultSize) {
      correctSize = false;
    }
    calculationValidation.setCalculationResultSize(
        correctSize, expectedResultSize, resultSize);
    log.trace("Leaving validateResultSize");
  }

  /** Gets the calculation results.
   *
   * @param merchantCalculation The merchant calculation.
   *
   * @return Returns the calculation results.
   */
  private List<Result> getResultList(
      final MendozaMerchantCalculation merchantCalculation) {
    log.trace("Entering getResultList");
    MerchantCalculationResults result =
      merchantCalculation.getMerchantCalculationResult();
     MerchantCalculationResults.Results results = result.getResults();
     log.trace("Leaving getResultList");
     return results.getResult();
  }

  /** Validates the response time of the calculation.
   *
   * @param merchantCalculation The merchant calculation.
   *
   * @param calculationValidation The result validation.
   */
  private void validateResponseTime(
      final MendozaMerchantCalculation merchantCalculation,
      final MendozaMerchantCalculationValidation calculationValidation) {
    log.trace("Entering validateResponseTime");
    if (merchantCalculation.getCalculationTime() > calculationTimeout) {
      calculationValidation.markCalculationAsTimeout();
    }
    log.trace("Leaving validateResponseTime");
  }

  /** Gets the calculation requested shipping method names.
   *
   * @param merchantCalculation The merchant calculation.
   *
   * @return Returns the requested shipping methods names.
   */
  private List<String> getRequestedMethodNameList(
      final MendozaMerchantCalculation merchantCalculation) {
    log.trace("Entering getRequestedMethodNameList");
    List<String> result = new ArrayList<String>();
    Calculate calculate = getCallbackCalculate(merchantCalculation);
    if (calculate != null) {
      Calculate.Shipping shippings = calculate.getShipping();
      List<Method> shippingMethods = shippings.getMethod();
      /** Get all the shipping methods from the callback */
      for (Method method : shippingMethods) {
        result.add(method.getName());
      }
    }
    log.trace("Leaving getRequestedMethodNameList");
    return result;
  }

  /** Gets the calculation requested address ids.
   *
   * @param merchantCalculation The merchant calculation.
   *
   * @return Returns the requested address ids.
   */
  private List<String> getRequestedAddressIdList(
      final MendozaMerchantCalculation merchantCalculation) {
    log.trace("Entering getRequestedAddressIdList");
    List<String> result = new ArrayList<String>();
    Calculate calculate = getCallbackCalculate(merchantCalculation);
    if (calculate != null) {
      List<AnonymousAddress> addresses =
        calculate.getAddresses().getAnonymousAddress();
      /** Get all the address id from the callback */
      for (AnonymousAddress address : addresses) {
        result.add(address.getId());
      }
    }
    log.trace("Leaving getRequestedAddressIdList");
    return result;
  }

  /** Gets the callback calculate.
   *
   * @param merchantCalculation The merchant calculation.
   *
   * @return Returns the callbak calculate.
   */
  private Calculate getCallbackCalculate(
      final MendozaMerchantCalculation merchantCalculation) {
    log.trace("Entering getCallbackCalculate");
    MerchantCalculationCallback callback =
      merchantCalculation.getMerchantCalculationCallback();
    log.trace("Leaving getCallbackCalculate");
    return callback.getCalculate();
  }
}
